/*-
 * Copyright (c) 2024-2025 Rozhuk Ivan <rozhuk.im@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * Author: Rozhuk Ivan <rozhuk.im@gmail.com>
 *
 */


#include <sys/param.h>
#include <sys/types.h>

#include <inttypes.h>
#include <stdlib.h> /* malloc, exit */
#include <stdio.h> /* snprintf, fprintf */
#include <unistd.h> /* close, write, sysconf */
#include <string.h> /* bcopy, bzero, memcpy, memmove, memset, strerror... */
#include <fcntl.h> /* open */
#include <errno.h>
#include <err.h>
#include <sysexits.h>
#include <pthread.h>
#include <paths.h> /* _PATH_VARRUN */
#include <getopt.h>
#include <pwd.h>
#include <grp.h>

#include "dev_midi.h"
#include "sys_utils.h"

#ifdef HAVE_CONFIG_H
#	include "config.h"
#endif

#ifndef PACKAGE_STRING
#	define PACKAGE_STRING		"virtual_midi"
#endif
#ifndef PACKAGE_DESCRIPTION
#	define PACKAGE_DESCRIPTION	"Create virtual MIDI device with software backend"
#endif

#define VIRTUAL_MIDI_DEF_VDEV		"midi"

/* See more: https://www.fluidsynth.org/api/settings_audio.html */
/* OSS */
#if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__OpenBSD__)
#	define VIRTUAL_MIDI_DEF_ODRV		"oss"
#	define VIRTUAL_MIDI_DEF_ODEV		"/dev/dsp"
#	define VIRTUAL_MIDI_DEF_SOUNDFONT_FILE	"/usr/local/share/sounds/sf2/FluidR3_GM.sf2"
#endif

/* ALSA */
#ifdef __linux__
#	define VIRTUAL_MIDI_DEF_ODRV		"alsa"
#	define VIRTUAL_MIDI_DEF_ODEV		"default"
#	define VIRTUAL_MIDI_DEF_SOUNDFONT_FILE	"/usr/share/sounds/sf2/FluidR3_GM.sf2"
#endif


static volatile int app_running = 1;

typedef struct command_line_options_s {
	int		daemon;
	const char	*pid;
	uid_t		pw_uid;		/* user uid */
	gid_t		pw_gid;		/* user gid */
	int		threads;
	const char	*vdev;
	/* snd backend settings. */
	const char	*odrv;
	const char	*odev;
	const char	*soundfont;
} cmd_opts_t, *cmd_opts_p;


static struct option long_options[] = {
	{ "help",	no_argument,		NULL,	'?'	},
	{ "daemon",	no_argument,		NULL,	'd'	},
	{ "pid",	required_argument,	NULL,	'p'	},
	{ "user",	required_argument,	NULL,	'u'	},
	{ "group",	required_argument,	NULL,	'g'	},
	{ "threads",	required_argument,	NULL,	't'	},
	{ "vdev",	required_argument,	NULL,	'V'	},
	{ "odrv",	required_argument,	NULL,	'o'	},
	{ "odev",	required_argument,	NULL,	'O'	},
	{ "soundfont",	required_argument,	NULL,	's'	},
	{ NULL,		0,			NULL,	0	}
};

static const char *long_options_descr[] = {
	"				Show help",
	"				Run as daemon",
	"<pid>				PID file name",
	"<user>			Change uid",
	"<group>			Change gid",
	"<cuse_threads>		CUSE threads count. Default: CPU count x2",
	"<virtual_device_name>		New virtual MIDI device base name. Default: " VIRTUAL_MIDI_DEF_VDEV,
	"<output_driver_name>		Output sound driver name. Default: " VIRTUAL_MIDI_DEF_ODRV,
	"<output_device_name>		Output device name. Default: " VIRTUAL_MIDI_DEF_ODEV,
	"<soundfont_file_name>	Soundfont file name. Default: " VIRTUAL_MIDI_DEF_SOUNDFONT_FILE,
	NULL
};


static int
cmd_opts_parse(int argc, char **argv, struct option *opts,
    cmd_opts_p cmd_opts) {
	int error, i, ch, opt_idx;
	char opts_str[1024];
	struct passwd *pwd, pwd_buf;
	struct group *grp, grp_buf;
	char tmbuf[4096];

	memset(cmd_opts, 0x00, sizeof(cmd_opts_t));
	cmd_opts->vdev = VIRTUAL_MIDI_DEF_VDEV;
	cmd_opts->odrv = VIRTUAL_MIDI_DEF_ODRV;
	cmd_opts->odev = VIRTUAL_MIDI_DEF_ODEV;
	cmd_opts->soundfont = VIRTUAL_MIDI_DEF_SOUNDFONT_FILE;

	/* Process command line. */
	/* Generate opts string from long options. */
	for (i = 0, opt_idx = 0;
	    NULL != opts[i].name && (int)(sizeof(opts_str) - 1) > opt_idx;
	    i ++) {
		if (0 == opts[i].val)
			continue;
		opts_str[opt_idx ++] = (char)opts[i].val;
		switch (opts[i].has_arg) {
		case optional_argument:
			opts_str[opt_idx ++] = ':';
			__attribute__((fallthrough)); /* PASSTROUTH. */
		case required_argument:
			opts_str[opt_idx ++] = ':';
			break;
		default:
			break;
		}
	}

	opts_str[opt_idx] = 0;
	opt_idx = -1;
	while ((ch = getopt_long_only(argc, argv, opts_str, opts,
	    &opt_idx)) != -1) {
restart_opts:
		switch (opt_idx) {
		case -1: /* Short option to index. */
			for (opt_idx = 0;
			    NULL != opts[opt_idx].name;
			    opt_idx ++) {
				if (ch == opts[opt_idx].val)
					goto restart_opts;
			}
			/* Unknown option. */
			break;
		case 0: /* help */
			return (EINVAL);
		case 1: /* daemon */
			cmd_opts->daemon = 1;
			break;
		case 2: /* pid */
			cmd_opts->pid = optarg;
			break;
		case 3: /* user */
			error = getpwnam_r(optarg, &pwd_buf, tmbuf, sizeof(tmbuf), &pwd);
			if (0 != error) {
				errx(EX_NOUSER,
				    "option \"-u\" requires valid UID, UID %s not found: %i - %s.",
				    optarg, error, strerror(error));
			}
			cmd_opts->pw_uid = pwd->pw_uid;
			break;
		case 4: /* group */
			error = getgrnam_r(optarg, &grp_buf, tmbuf, sizeof(tmbuf), &grp);
			if (0 != error) {
				errx(EX_NOUSER,
				    "option \"-g\" requires valid GID, GID %s not found: %i - %s.",
				    optarg, error, strerror(error));
			}
			cmd_opts->pw_gid = grp->gr_gid;
			break;
		case 5: /* threads */
			cmd_opts->threads = atoi(optarg);
			break;
		case 6: /* vdev */
			cmd_opts->vdev = optarg;
			break;
		case 7: /* odrv */
			cmd_opts->odrv = optarg;
			break;
		case 8: /* odev */
			cmd_opts->odev = optarg;
			break;
		case 9: /* soundfont */
			cmd_opts->soundfont = optarg;
			break;
		default:
			return (EINVAL);
		}
		opt_idx = -1;
	}

	return (0);
}

static void
signal_handler(int sig) {

	switch (sig) {
	case SIGINT:
	case SIGTERM:
	case SIGKILL:
		app_running = 0;
		break;
	case SIGHUP:
	case SIGUSR1:
	case SIGUSR2:
	default:
		break;
	}
}

static void *
cuse_worker_proc(void *arg __unused) {

	while (0 != app_running) {
		if (0 != cuse_wait_and_process())
			break;
	}

	return (NULL);
}


int
main(int argc, char **argv) {
	int error = 0;
	int fd_lock_file = -1 /*, fd_midistat = -1; TODO in kernel. */;
	cmd_opts_t cmd_opts;
	vmb_options_t vmb_opts;
	struct cuse_dev *midi_dev;
	pthread_t td;
	char lock_file[PATH_MAX];
	const char *odev_ptr;
	const char *forbidden_chars = "/\\ .";
	size_t i, lock_file_size;
	struct timespec rqts = { .tv_sec = 3600, .tv_nsec = 0 };

	/* Command line processing. */
	error = cmd_opts_parse(argc, argv, long_options, &cmd_opts);
	if (0 != error) {
		if (-1 == error)
			return (0); /* Handled action. */
		print_usage(argv[0], PACKAGE_STRING, PACKAGE_DESCRIPTION,
		    long_options, long_options_descr);
		return (error);
	}
	/* Handle cmd line options. */
	if (NULL == cmd_opts.vdev ||
	    NULL == cmd_opts.odrv ||
	    NULL == cmd_opts.odev) {
		fprintf(stderr, "vdev, odrv and odev is required options!\n");
		print_usage(argv[0], PACKAGE_STRING, PACKAGE_DESCRIPTION,
		    long_options, long_options_descr);
		return (-1);
	}
	if (0 == cmd_opts.threads) {
		cmd_opts.threads = (int)sysconf(_SC_NPROCESSORS_ONLN);
		if (-1 == cmd_opts.threads) {
			cmd_opts.threads = 32;
		} else {
			cmd_opts.threads *= 2;
		}
	}

	/* Lock file. */
	odev_ptr = cmd_opts.odev;
	if (0 == strncasecmp("/dev/", odev_ptr, 5)) {
		odev_ptr += 5; /* remove "/dev/" */
	}
	lock_file_size = (size_t)snprintf(lock_file, sizeof(lock_file),
	    _PATH_VARRUN "%s-%s-%s.lock",
	    cmd_opts.vdev, cmd_opts.odrv, odev_ptr);
	/* Replace special characters. */
	for (i = (sizeof(_PATH_VARRUN) - 1); i < (lock_file_size - 5); i ++) {
		if (NULL == strchr(forbidden_chars, lock_file[i]))
			continue;
		lock_file[i] = '_';
	}
	/* Start file locking... */
	fd_lock_file = open(lock_file, (O_RDWR | O_CREAT));
	if (-1 == fd_lock_file) {
		errx(EX_CANTCREAT, "Could not create lock file: %s - %i: %s",
		    lock_file, errno, strerror(errno));
	}
	if (0 != flock(fd_lock_file, (LOCK_EX | LOCK_NB))) {
		errx(EX_TEMPFAIL, "Could not lock file, probably other process already running: %s - %i: %s",
		    lock_file, errno, strerror(errno));
	}

	/* Daemonize. */
	if (0 != cmd_opts.daemon) {
		make_daemon();
		signal(SIGINT, signal_handler);
		signal(SIGTERM, signal_handler);
		signal(SIGHUP, signal_handler);
		signal(SIGUSR1, signal_handler);
		signal(SIGUSR2, signal_handler);
		signal(SIGPIPE, SIG_IGN);
	}
	/* PID file. */
	if (NULL != cmd_opts.pid) {
		write_pid(cmd_opts.pid);
	}

	/* CUSE init. */
	if (0 != cuse_init()) {
		errx(EX_OSERR, "Could not connect to CUSE module - %i: %s",
		    errno, strerror(errno));
	}

	/* MIDI device. */
	memset(&vmb_opts, 0x00, sizeof(vmb_options_t));
	vmb_opts.driver = cmd_opts.odrv;
	vmb_opts.device = cmd_opts.odev;
	vmb_opts.soundfont = cmd_opts.soundfont;
	midi_dev = vm_dev_midi_create(cmd_opts.vdev, &vmb_opts);
	if (NULL == midi_dev) {
		errx(EX_SOFTWARE, "Could not create '/dev/%s' - %i: %s",
		    cmd_opts.vdev, errno, strerror(errno));
	}

	/* CUSE post init. */
	for (i = 0; i < (size_t)cmd_opts.threads; i ++) {
		pthread_create(&td, NULL, &cuse_worker_proc, NULL);
	}

	/* Drop rights. */
	set_user_and_group(cmd_opts.pw_uid, cmd_opts.pw_gid);

	/* Wait for signals. */
	while (0 != app_running) {
		nanosleep(&rqts, NULL); /* Ignore early wakeup and errors. */
	}

	/* Exititng... */
	vm_dev_midi_destroy(midi_dev);
	close(fd_lock_file);
	unlink(lock_file);
	if (NULL != cmd_opts.pid) {
		unlink(cmd_opts.pid);
	}

	return (error);
}
